//
//  KDiTunesLibrary.m
//  Source List
//
//  Created by Keith Duncan on 05/03/2008.
//  Copyright 2008 thirty-three. All rights reserved.
//

#import "iTunesLibrary.h"

#import "iTunesNode.h"

#import "AFStackDictionary.h"
#import "AFKeyIndexedArray.h"

@interface iTunesLibrary ()
@property (retain, readwrite) AFSourceNode *rootNode;
@end

@implementation iTunesLibrary

@dynamic rootNode;

#define DICTIONARY_PARSER 0
#define SAX_PARSER 1

#define PARSER_TYPE DICTIONARY_PARSER

#pragma mark -

#if (PARSER_TYPE==SAX_PARSER)

enum {
	START = 0,
	INSIDE_PLIST = 0x01,
	INSIDE_ROOT = 0x02,
	PARSING_KEY = 0x10,
	END = 0,
};
typedef NSUInteger PlistParserState;

struct PlistParserContext {
	NSPointerArray *classStack;
	PlistParserState currentState;
};

void *CreateXMLStructureCallBack(CFXMLParserRef parser, CFXMLNodeRef node, void *info) {
	struct PlistParserContext *context = (struct PlistParserContext *)info;
	
	if (context->currentState == START) {
		if ([(id)CFXMLNodeGetString(node) isEqualToString:@"plist"]) {
			context->currentState = INSIDE_PLIST;
			return [[[NSMutableArray alloc] init] autorelease];
		} else return NULL;
	} else if (context->currentState == INSIDE_PLIST) context->currentState = (context->currentState | INSIDE_ROOT);
	
	BOOL insidePlist = ((context->currentState & INSIDE_PLIST) == INSIDE_PLIST);
	if (!insidePlist) return NULL;
	
	CFXMLNodeTypeCode type = CFXMLNodeGetTypeCode(node);
	NSString *elementName = (NSString *)CFXMLNodeGetString(node);
	
	if (type == kCFXMLNodeTypeElement) {
		id object = nil;
		
		if ([context->classStack pointerAtIndex:([context->classStack count]-1)] == [StackDictionary class]) {
			if ([elementName isEqualToString:@"key"]) {
				context->currentState = (context->currentState | PARSING_KEY);
				
				object = [[[NSMutableString alloc] init] autorelease];
			} else {
				context->currentState = (context->currentState & ~PARSING_KEY);
			}
		}
		
		if (object == nil) {
			if ([elementName isEqualToString:@"dict"]) {
				object = [[[StackDictionary alloc] init] autorelease];
			} else if ([elementName isEqualToString:@"array"]) {
				object = [[[NSMutableArray alloc] init] autorelease];
			} else if ([elementName isEqualToString:@"string"]) {
				object = [[[NSMutableString alloc] init] autorelease];
			} else if ([elementName isEqualToString:@"data"]) {
				object = [[[NSMutableData alloc] init] autorelease];
			} else if ([elementName isEqualToString:@"true"]) {
				object = (id)kCFBooleanTrue; 
			} else if ([elementName isEqualToString:@"false"]) {
				object = (id)kCFBooleanFalse;
			} else if ([elementName isEqualToString:@"integer"]) {
				[context->classStack addPointer:[NSNumber class]];
				object = [[[NSMutableString alloc] init] autorelease];
				
				return object;
			} else if ([elementName isEqualToString:@"date"]) {
				[context->classStack addPointer:[NSDate class]];
				object = [[[NSMutableString alloc] init] autorelease];
				
				return object;
			} else {
				return NULL;
			}				
		}
		
		[context->classStack addPointer:[object class]];
		return object;
	} else if (type == kCFXMLNodeTypeText) {
		return elementName;
	}
	
	return NULL;
}

void AddChildCallBack(CFXMLParserRef parser, void *parent, void *child, void *info) {
	if (parent == NULL) return;
	
	struct PlistParserContext *context = info;
	
	if ((context->currentState & PARSING_KEY) == PARSING_KEY) {
		[(StackDictionary *)parent pushKey:child];
	} else {
		[(id)parent addObject:child];
	}
}

void EndXMLStructureCallBack(CFXMLParserRef parser, void *xmlType, void *info) {
	struct PlistParserContext *context = info;
	[context->classStack removePointerAtIndex:[context->classStack count]];
}

#endif

#pragma mark -

- (BOOL)loadAndReturnError:(NSError **)error {
	if (![super loadAndReturnError:error]) return NO;
	
	AFSourceNode *sourceNodes[3];
	
	sourceNodes[0] = [iTunesNode sourceNodeWithName:@"Library" representedObject:nil];
	[sourceNodes[0] setType:kiTunesLibrary position:0];
	
	sourceNodes[1] = [iTunesNode sourceNodeWithName:@"Store" representedObject:nil];
	[sourceNodes[1] setType:kiTunesLibrary position:1];
	
	sourceNodes[2] = [iTunesNode sourceNodeWithName:@"Playlists" representedObject:nil];
	[sourceNodes[2] setType:kiTunesLibrary position:2];
	
	AFSourceNode *rootNode = [AFSourceNode sourceNodeWithName:nil representedObject:nil];
	[[rootNode mutableChildNodes] setArray:[NSArray arrayWithObjects:sourceNodes count:3]];
	
	@try {
#if (PARSER_TYPE == DICTIONARY_PARSER)
		_library = [[NSDictionary alloc] initWithContentsOfFile:_path];
#elif (PARSER_TYPE == SAX_PARSER)
		CFXMLParserCallBacks callbacks;
		(callbacks).createXMLStructure = CreateXMLStructureCallBack;
		(callbacks).addChild = AddChildCallBack;
		(callbacks).endXMLStructure = EndXMLStructureCallBack;
		
		int fd = 0;
		CFXMLParserRef parser = NULL;
		
		struct PlistParserContext context;
		memset(&context, 0, sizeof(struct PlistParserContext));
		
		CFXMLParserContext parserContext;
		memset(&parserContext, 0, sizeof(CFXMLParserContext));
		parserContext.info = &context;
		
		@try {
			fd = open([_path fileSystemRepresentation], O_RDONLY, O_EXLOCK);
			NSData *libraryData = [NSData dataWithContentsOfMappedFile:_path];
			
			context.classStack = [[[NSPointerArray alloc] initWithOptions:(NSPointerFunctionsOpaqueMemory | NSPointerFunctionsObjectPointerPersonality)] autorelease];
			
			parser = CFXMLParserCreate(kCFAllocatorDefault, (CFDataRef)libraryData, NULL, (CFXMLParserOptions)(kCFXMLParserSkipMetaData | kCFXMLParserSkipWhitespace), kCFXMLNodeCurrentVersion, &callbacks, &parserContext);
			if (!CFXMLParserParse(parser)) [NSException raise:NSParseErrorException format:@"%s, the library couldn't be parsed", __PRETTY_FUNCTION__, nil];
			
			_library = CFXMLParserGetDocument(parser);
		}
		@finally {
			if (fd != 0) close(fd);
			if (parser != NULL) CFRelease(parser);
		}
#else
#error no parser specified	
#endif
		
		NSArray *playlists = [_library objectForKey:@"Playlists"];
		NSMutableSet *folders = [NSMutableSet set], *nestedItems = [NSMutableSet set];
		
		for (NSDictionary *playlist in playlists) {
			if ([playlist objectForKey:@"Visible"] != nil && ![[playlist objectForKey:@"Visible"] boolValue]) continue;
			
			AFSourceNode *parent = sourceNodes[0];
			iTunesNode *currentNode = [iTunesNode sourceNodeWithName:[playlist objectForKey:@"Name"] representedObject:playlist];
			
			NSInteger kind = NSNotFound;
			id kindObject = [playlist objectForKey:@"Distinguished Kind"];
			if (kindObject != nil) kind = [kindObject integerValue];
			
			switch (kind) {
				case /* Music */ 4:
					[currentNode setType:kiTunesLibrary position:0];
					break;
				case /* Movies */ 2:
					[currentNode setType:kiTunesLibrary position:1];
					break;
				case /* TV Shows */ 3:
					[currentNode setType:kiTunesLibrary position:2];
					break;
				case /* Podcasts */ 10:
					_podcasts = currentNode;
					[currentNode setType:kiTunesLibrary position:3];
					NSMutableDictionary *podcasts = [NSMutableDictionary dictionary];
					
					NSDictionary *tracks = [_library objectForKey:@"Tracks"];
					NSArray *episodes = [playlist objectForKey:@"Playlist Items"];
					
					for (NSDictionary *currentEpisode in episodes) {
						NSDictionary *track = [tracks objectForKey:[[currentEpisode objectForKey:@"Track ID"] stringValue]];
						
						NSString *podcastTitle = [track objectForKey:@"Album"];
						if (podcastTitle == nil || [podcasts objectForKey:podcastTitle] != nil) continue;
						
						iTunesNode *currentPodcast = [iTunesNode sourceNodeWithName:podcastTitle representedObject:currentEpisode];
						currentPodcast.type = kiTunesPodcast;
						
						[podcasts setValue:currentPodcast forKey:podcastTitle];
					}
					
					[[currentNode mutableChildNodes] addObjectsFromArray:[podcasts allValues]];
					
					break;
				case /* AudioBooks*/ 5:
					[currentNode setType:kiTunesLibrary position:4];
					break;
				case /* Purchased */ 19:
					[currentNode setType:kiTunesLibrary position:0];
					parent = sourceNodes[1];
					break;
				case /* Purchased on [...] */ 20:
					[currentNode setType:kiTunesLibrary position:1];
					parent = sourceNodes[1];
					break;
				case /* Party Shuffle */ 22:
					[currentNode setType:kiTunesLibrary position:0];
					parent = sourceNodes[2];
					break;
				default:
					if ([playlist objectForKey:@"Smart Info"] != nil) {
						currentNode.type = kiTunesSmartPlaylist;
					} else if ([[playlist objectForKey:@"Folder"] boolValue]) {				
						currentNode.type = kiTunesFolder;
						
						[folders addObject:currentNode];
					}
					
					if ([playlist objectForKey:@"Parent Persistent ID"] != nil) {
						[nestedItems addObject:currentNode];
						continue;
					}
					
					parent = sourceNodes[2];
					break;
			}
			
			[[parent mutableChildNodes] addObject:currentNode];
		}
		
		for (iTunesNode *currentFolder in folders) {
			id currentFolderID = [currentFolder.representedObject objectForKey:@"Playlist Persistent ID"];
			
			for (iTunesNode *currentNode in nestedItems)
				if ([[currentNode.representedObject objectForKey:@"Parent Persistent ID"] isEqual:currentFolderID])
					[[currentFolder mutableChildNodes] addObject:currentNode];
		}
		
		NSArray *sortDescriptors = [NSArray arrayWithObjects:
									[[[NSSortDescriptor alloc] initWithKey:@"type" ascending:NO] autorelease],
									[[[NSSortDescriptor alloc] initWithKey:@"position" ascending:YES] autorelease],
									[[[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES selector:@selector(caseInsensitiveCompare:)] autorelease], nil];
		
		[rootNode sortWithSortDescriptors:sortDescriptors recursively:YES];
	}
	@catch (NSException *exception) {
		NSLog(@"%@", exception);
		
		*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:nil];
		return NO;
	}
	
	self.rootNode = rootNode;
	return YES;
}

- (BOOL)playlistIsPodcast:(NSString *)playlistName {
	for (iTunesNode *podcast in [_podcasts childNodes])
		if ([[podcast name] isEqualToString:playlistName])
			return YES;
	
	return NO;
}

#pragma mark -

- (NSString *)name {
	return @"iTunes";
}

- (NSString *)expectedPath {
	return [NSHomeDirectory() stringByAppendingPathComponent:@"/Music/iTunes/iTunes Music Library.xml"];
}

- (NSArray *)additionalSearchPaths {
	NSDictionary *iAppPreferences = [[NSUserDefaults standardUserDefaults] persistentDomainForName:@"com.apple.iApps"];
	return [iAppPreferences objectForKey:@"iTunesRecentDatabasePaths"];
}

@end
